#!/usr/bin/env python

# pylint: disable=E1121

'''

seaf-cli is command line interface for seafile client.

Subcommands:

    init:           create config files for seafile client
    start:          start and run seafile client as daemon
    stop:           stop seafile client
    list:           list local liraries
    status:         show syncing status
    download:       download a library from seafile server
    sync:           synchronize an existing folder with a library in
                        seafile server
    desync:         desynchronize a library with seafile server
    create:         create a new library


Detail
======

Seafile client stores all its configure information in a config dir. The default location is `~/.ccnet`. All the commands below accept an option `-c <config-dir>`.

init
----
Initialize seafile client. This command initializes the config dir. It also creates sub-directories `seafile-data` and `seafile` under `parent-dir`. `seafile-data` is used to store internal data, while `seafile` is used as the default location put downloaded libraries.

    seaf-cli init [-c <config-dir>] -d <parent-dir>

start
-----
Start seafile client. This command start `ccnet` and `seaf-daemon`, `ccnet` is the network part of seafile client, `seaf-daemon` manages the files.

    seaf-cli start [-c <config-dir>]

stop
----
Stop seafile client.

    seaf-cli stop [-c <config-dir>]


Download
--------
Download a library from seafile server

    seaf-cli download -l <library-id> -s <seahub-server-url> -d <parent-directory> -u <username> -p <password>


sync
----
Synchronize a library with an existing folder.

    seaf-cli sync -l <library-id> -s <seahub-server-url> -d <existing-folder> -u <username> -p <password>

desync
------
Desynchronize a library from seafile server

    seaf-cli desync -d <existing-folder>

create
------
Create a new library

    seaf-cli create -s <seahub-server-url> -n <library-name> -u <username> -p <password> -t <description> [-e <library-password>]

'''

import argparse
import os
import json
import subprocess
import sys
import time
import getpass
import urllib
import urllib2
from urlparse import urlparse

import ccnet
import seafile

if 'HOME' in os.environ:
    DEFAULT_CONF_DIR = "%s/.ccnet" % os.environ['HOME']
else:
    DEFAULT_CONF_DIR = None

seafile_datadir = None
seafile_worktree = None


def _check_seafile():
    ''' Check ccnet and seafile have been installed '''

    dirs = os.environ['PATH'].split(':')
    def exist_in_path(prog):
        ''' Check whether 'prog' exists in system path '''
        for d in dirs:
            if d == '':
                continue
            path = os.path.join(d, prog)
            if os.path.exists(path):
                return True

    progs = [ 'ccnet', 'ccnet-init', 'seaf-daemon' ]

    for prog in progs:
        if not exist_in_path(prog):
            print "%s not found in PATH. Have you installed seafile?" % prog
            sys.exit(1)


def _config_valid(conf):
    ''' Check config directory valid '''

    if not os.path.exists(conf) or not os.path.isdir(conf):
        print "%s not exists" % conf
        return False

    config_conf = conf + "/ccnet.conf"
    seafile_ini = conf + "/seafile.ini"
    if not os.path.exists(config_conf):
        print "Could not load %s" % config_conf
        return False
    if not os.path.exists(seafile_ini):
        print "Could not load %s" % seafile_ini
        return False

    with open(seafile_ini) as f:
        for line in f:
            global seafile_datadir, seafile_worktree
            seafile_datadir = line.strip()
            seafile_worktree = os.path.join(
                os.path.dirname(seafile_datadir), "seafile")
            break

    if not seafile_datadir or not seafile_worktree:
        print "Could not load seafile_datadir and seafile_worktree"
        return False
    return True

def run_argv(argv, cwd=None, env=None, suppress_stdout=False, suppress_stderr=False):
    '''Run a program and wait it to finish, and return its exit code. The
    standard output of this program is supressed.

    '''
    with open(os.devnull, 'w') as devnull:
        if suppress_stdout:
            stdout = devnull
        else:
            stdout = sys.stdout

        if suppress_stderr:
            stderr = devnull
        else:
            stderr = sys.stderr

        proc = subprocess.Popen(argv,
                                cwd=cwd,
                                stdout=stdout,
                                stderr=stderr,
                                env=env)
        return proc.wait()

def get_env():
    env = dict(os.environ)
    ld_library_path = os.environ.get('SEAFILE_LD_LIBRARY_PATH', '')
    if ld_library_path:
        env['LD_LIBRARY_PATH'] = ld_library_path

    return env

def urlopen(url, data=None, headers=None):
    if data:
        data = urllib.urlencode(data)
    headers = headers or {}
    req = urllib2.Request(url, data=data, headers=headers)
    resp = urllib2.urlopen(req)

    return resp.read()

SEAF_CLI_VERSION = ""

def get_peer_id(conf_dir):
    pool = ccnet.ClientPool(conf_dir)
    ccnet_rpc = ccnet.CcnetRpcClient(pool)
    info = ccnet_rpc.get_session_info()
    return info.id

def get_token(url, username, password, conf_dir):
    platform = 'linux'
    device_id = get_peer_id(conf_dir)
    device_name = 'terminal-' + os.uname()[1]
    client_version = SEAF_CLI_VERSION
    platform_version = ''
    data = {
        'username': username,
        'password': password,
        'platform': platform,
        'device_id': device_id,
        'device_name': device_name,
        'client_version': client_version,
        'platform_version': platform_version,
    }
    token_json = urlopen("%s/api2/auth-token/" % url, data=data)
    tmp = json.loads(token_json)
    token = tmp['token']
    return token

def get_repo_downlod_info(url, token):
    headers = { 'Authorization': 'Token %s' % token }
    repo_info = urlopen(url, headers=headers)
    return json.loads(repo_info)

def seaf_init(args):
    ''' initialize config directorys'''

    ccnet_conf_dir = DEFAULT_CONF_DIR
    if args.confdir:
        ccnet_conf_dir = args.confdir
    if args.dir:
        seafile_path = args.dir
    else:
        print "Must specify the parent path for put seafile-data"
        sys.exit(0)
    seafile_path = os.path.abspath(seafile_path)

    if os.path.exists(ccnet_conf_dir):
        print "%s already existed" % ccnet_conf_dir
        sys.exit(0)

    cmd = [ "ccnet-init", "-c", ccnet_conf_dir, "-n", "anonymous" ]
    if run_argv(cmd, env=get_env()) != 0:
        print 'failed to init ccnet'
        sys.exit(1)

    if not os.path.exists(seafile_path):
        print "%s not exists" % seafile_path
        sys.exit(0)
    seafile_ini = ccnet_conf_dir + "/seafile.ini"
    seafile_data = seafile_path + "/seafile-data"
    fp = open(seafile_ini, 'w')
    fp.write(seafile_data)
    fp.close()
    print "Writen seafile data directory %s to %s" % (seafile_data, seafile_ini)


def seaf_start_all(args):
    ''' start ccnet and seafile daemon '''

    seaf_start_ccnet(args)
    # wait ccnet process
    time.sleep(1)
    seaf_start_seafile(args)


def seaf_start_ccnet(args):
    ''' start ccnet daemon '''

    conf_dir = DEFAULT_CONF_DIR
    if args.confdir:
        conf_dir = args.confdir
    conf_dir = os.path.abspath(conf_dir)

    if not _config_valid(conf_dir):
        print "Invalid config directory"
        sys.exit(1)

    print "starting ccnet daemon ..."

    cmd = [ "ccnet", "--daemon", "-c", conf_dir ]
    if run_argv(cmd, env=get_env()) != 0:
        print 'CCNet daemon failed to start.'
        sys.exit(1)

    print "Started: ccnet daemon ..."

def seaf_start_seafile(args):
    ''' start seafile daemon '''

    conf_dir = DEFAULT_CONF_DIR
    if args.confdir:
        conf_dir = args.confdir
    conf_dir = os.path.abspath(conf_dir)

    if not _config_valid(conf_dir):
        print "Invalid config directory"
        sys.exit(1)

    print "starting seafile daemon ..."

    cmd = [ "seaf-daemon", "--daemon", "-c", conf_dir, "-d", seafile_datadir,
            "-w", seafile_worktree ]
    if run_argv(cmd, env=get_env()) != 0:
        print 'Failed to start seafile daemon'
        sys.exit(1)

    print "Started: seafile daemon ..."

def seaf_stop(args):
    '''stop seafile daemon '''

    conf_dir = DEFAULT_CONF_DIR
    if args.confdir:
        conf_dir = args.confdir
    conf_dir = os.path.abspath(conf_dir)

    if not _config_valid(conf_dir):
        print "Invalid config directory"
        sys.exit(1)

    pool = ccnet.ClientPool(conf_dir)
    client = pool.get_client()
    try:
        client.send_cmd("shutdown")
    except:
        # ignore NetworkError("Failed to read from socket")
        pass


def seaf_list(args):
    '''List local libraries'''

    conf_dir = DEFAULT_CONF_DIR
    if args.confdir:
        conf_dir = args.confdir
    conf_dir = os.path.abspath(conf_dir)

    if not _config_valid(conf_dir):
        print "Invalid config directory"
        sys.exit(1)

    pool = ccnet.ClientPool(conf_dir)
    seafile_rpc = seafile.RpcClient(pool, req_pool=False)

    repos = seafile_rpc.get_repo_list(-1, -1)
    print "Name\tID\tPath"
    for repo in repos:
        print repo.name, repo.id, repo.worktree

def get_base_url(url):
    parse_result = urlparse(url)
    scheme = parse_result.scheme
    netloc = parse_result.netloc

    if scheme and netloc:
        return '%s://%s' % (scheme, netloc)

    return None

def seaf_download(args):
    '''Download a library from seafile server '''

    conf_dir = DEFAULT_CONF_DIR
    if args.confdir:
        conf_dir = args.confdir
    conf_dir = os.path.abspath(conf_dir)

    if not _config_valid(conf_dir):
        print "Invalid config directory"
        sys.exit(1)

    repo = args.library
    if not repo:
        print "Library id is required"
        sys.exit(1)

    url = args.server
    if not url:
        print "Seafile server url need to be presented"
        sys.exit(1)

    download_dir = seafile_worktree
    if args.dir:
        download_dir = os.path.abspath(args.dir)


    pool = ccnet.ClientPool(conf_dir)
    seafile_rpc = seafile.RpcClient(pool, req_pool=False)

    username = args.username
    if not username:
        username = raw_input("Enter username: ")
    password = args.password
    if not password:
        password = getpass.getpass("Enter password for user %s : " % username)

    # curl -d 'username=<USERNAME>&password=<PASSWORD>' http://127.0.0.1:8000/api2/auth-token
    token = get_token(url, username, password, conf_dir)

    tmp = get_repo_downlod_info("%s/api2/repos/%s/download-info/" % (url, repo), token)

    encrypted = tmp['encrypted']
    magic = tmp.get('magic', None)
    enc_version = tmp.get('enc_version', None)
    random_key = tmp.get('random_key', None)

    clone_token = tmp['token']
    relay_id = tmp['relay_id']
    relay_addr = tmp['relay_addr']
    relay_port = tmp['relay_port']
    email = tmp['email']
    repo_name = tmp['repo_name']
    version = tmp.get('repo_version', 0)

    more_info = None
    use_http = seafile_rpc.get_config("enable_http_sync")
    if use_http and bool(use_http):
        base_url = get_base_url(url)
        if base_url:
            more_info = json.dumps({'server_url': base_url})

    print "Starting to download ..."
    print "Library %s will be downloaded to %s" % (repo, download_dir)
    if encrypted == 1:
        repo_passwd = getpass.getpass("Enter password for the library: ")
    else:
        repo_passwd = None

    seafile_rpc.download(repo,
                         version,
                         relay_id,
                         repo_name.encode('utf-8'),
                         download_dir.encode('utf-8'),
                         clone_token,
                         repo_passwd, magic,
                         relay_addr,
                         relay_port,
                         email, random_key, enc_version, more_info)



def seaf_sync(args):
    ''' synchronize a library from seafile server '''

    conf_dir = DEFAULT_CONF_DIR
    if args.confdir:
        conf_dir = args.confdir
    conf_dir = os.path.abspath(conf_dir)

    if not _config_valid(conf_dir):
        print "Invalid config directory"
        sys.exit(1)

    repo = args.library
    if not repo:
        print "Library id is required"
        sys.exit(1)

    url = args.server
    if not url:
        print "Seafile server url is required"
        sys.exit(1)

    folder = args.folder
    if not folder:
        print "The local directory is required"
        sys.exit(1)

    folder = os.path.abspath(folder)
    if not os.path.exists(folder):
        print "The local directory does not exists"
        sys.exit(1)

    pool = ccnet.ClientPool(conf_dir)
    seafile_rpc = seafile.RpcClient(pool, req_pool=False)

    username = args.username
    if not username:
        username = raw_input("Enter username: ")
    password = args.password
    if not password:
        password = getpass.getpass("Enter password for user %s : " % username)

    token = get_token(url, username, password, conf_dir)
    tmp = get_repo_downlod_info("%s/api2/repos/%s/download-info/" % (url, repo), token)

    encrypted = tmp['encrypted']
    magic = tmp.get('magic', None)
    enc_version = tmp.get('enc_version', None)
    random_key = tmp.get('random_key', None)

    clone_token = tmp['token']
    relay_id = tmp['relay_id']
    relay_addr = tmp['relay_addr']
    relay_port = tmp['relay_port']
    email = tmp['email']
    repo_name = tmp['repo_name']
    version = tmp.get('repo_version', 0)

    more_info = None
    use_http = seafile_rpc.get_config("enable_http_sync")
    if use_http and bool(use_http):
        base_url = get_base_url(url)
        if base_url:
            more_info = json.dumps({'server_url': url})

    print "Starting to download ..."
    if encrypted == 1:
        repo_passwd = getpass.getpass("Enter password for the library: ")
    else:
        repo_passwd = None

    seafile_rpc.clone(repo,
                      version,
                      relay_id,
                      repo_name.encode('utf-8'),
                      folder,
                      clone_token,
                      repo_passwd, magic,
                      relay_addr,
                      relay_port,
                      email, random_key, enc_version, more_info)


def seaf_desync(args):
    '''Desynchronize a library from seafile server'''

    conf_dir = DEFAULT_CONF_DIR
    if args.confdir:
        conf_dir = args.confdir
    conf_dir = os.path.abspath(conf_dir)

    if not _config_valid(conf_dir):
        print "Invalid config directory"
        sys.exit(1)

    repo_path = args.folder
    if not repo_path:
        print "Must specify the local path of the library"
        sys.exit(1)
    repo_path = os.path.abspath(repo_path)

    pool = ccnet.ClientPool(conf_dir)
    seafile_rpc = seafile.RpcClient(pool, req_pool=False)

    repos = seafile_rpc.get_repo_list(-1, -1)
    repo = None
    for r in repos:
        if r.worktree == repo_path.decode('utf-8'):
            repo = r
            break

    if repo:
        print "Desynchronize %s" % repo.name
        seafile_rpc.remove_repo(repo.id)
    else:
        print "%s is not a library" % args.folder


def seaf_config(args):
    '''Configure the seafile client'''

    conf_dir = DEFAULT_CONF_DIR
    if args.confdir:
        conf_dir = args.confdir
    conf_dir = os.path.abspath(conf_dir)

    if not _config_valid(conf_dir):
        print "Invalid config directory"
        sys.exit(1)

    config_key = args.key
    if not config_key:
        print "Must specify configuration key"
        sys.exit(1)

    config_value = args.value

    pool = ccnet.ClientPool(conf_dir)
    seafile_rpc = seafile.RpcClient(pool, req_pool=False)

    if config_value:
        # set configuration key
        seafile_rpc.seafile_set_config(config_key, config_value)
    else:
        # print configuration key
        val = seafile_rpc.seafile_get_config(config_key)
        print "%s = %s" % (config_key, val)


def seaf_status(args):
    '''Show status'''

    conf_dir = DEFAULT_CONF_DIR
    if args.confdir:
        conf_dir = args.confdir
    conf_dir = os.path.abspath(conf_dir)

    if not _config_valid(conf_dir):
        print "Invalid config directory"
        sys.exit(1)

    pool = ccnet.ClientPool(conf_dir)
    ccnet_rpc = ccnet.CcnetRpcClient(pool, req_pool=False)
    seafile_rpc = seafile.RpcClient(pool, req_pool=False)

    tasks = seafile_rpc.get_clone_tasks()
    print "# Name\tStatus\tProgress"
    for task in tasks:
        if task.state == "fetch":
            tx_task = seafile_rpc.find_transfer_task(task.repo_id)
            print "%s\t%s\t%d/%d, %.1fKB/s" % (task.repo_name, "downloading",
                                        tx_task.block_done, tx_task.block_total,
                                        tx_task.rate/1024.0)
        elif task.state == "checkout":
            checkout_task = seafile_rpc.get_checkout_task(task.repo_id)
            print "%s\t%s\t%d/%d" % (task.repo_name, "checkout",
                                   checkout_task.finished_files,
                                   checkout_task.total_files)
        elif task.state == "error":
            tx_task = seafile_rpc.find_transfer_task(task.repo_id)
            if tx_task:
                err = tx_task.error_str
            else:
                err = task.error_str
            print "%s\t%s\t%s" % (task.repo_name, "error", err)
        elif task.state == 'done':
            # will be shown in repo status
            pass
        else:
            print "%s\t%s" % (task.repo_name, "unknown")

    # show repo status
    print ""
    print "# Name\tStatus"
    repos = seafile_rpc.get_repo_list(-1, -1)
    for repo in repos:
        auto_sync_enabled = seafile_rpc.is_auto_sync_enabled()
        if not auto_sync_enabled or not repo.auto_sync:
            print "%s\t%s" % (repo.name, "auto sync disabled")
            continue

        relay = ccnet_rpc.get_peer(repo.props.relay_id)
        t = seafile_rpc.get_repo_sync_task(repo.id)
        if not relay.is_ready:
            print "%s\t%s" % (repo.name, "connecting server")
        else:
            if not t:
                # Relay is ready, but the sync task is not scheduled yet
                # (shortly after seaf-daemon is started)
                print "%s\t%s" % (repo.name, "initializing")
            elif t.state == "error":
                print "%s\t%s" % (repo.name, t.error)
            else:
                print "%s\t%s" % (repo.name, t.state)


def create_repo(url, token, args):
    headers = { 'Authorization': 'Token %s' % token }
    data = {
        'name': args.name,
        'desc': args.desc,
    }
    if args.libpasswd:
        data['passwd'] = args.libpasswd
    repo_info_json =  urlopen(url, data=data, headers=headers)
    repo_info = json.loads(repo_info_json)
    return repo_info['repo_id']

def seaf_create(args):
    '''Create a library'''
    conf_dir = DEFAULT_CONF_DIR
    if args.confdir:
        conf_dir = args.confdir
    conf_dir = os.path.abspath(conf_dir)

    # check username and password
    username = args.username
    if not username:
        username = raw_input("Enter username: ")
    password = args.password
    if not password:
        password = getpass.getpass("Enter password for user %s " % username)

    # check url
    url = args.server
    if not url:
        print "Seafile server url need to be presented"
        sys.exit(1)

    # curl -d 'username=<USERNAME>&password=<PASSWORD>' http://127.0.0.1:8000/api2/auth-token
    token = get_token(url, username, password, conf_dir)

    repo_id = create_repo("%s/api2/repos/" % (url), token, args)
    print repo_id


def main():
    ''' Main entry '''

    _check_seafile()

    parser = argparse.ArgumentParser()
    subparsers = parser.add_subparsers(title='subcommands', description='')

    confdir_required = DEFAULT_CONF_DIR is None

    # init
    parser_init = subparsers.add_parser('init', help='Initialize config directory')
    parser_init.set_defaults(func=seaf_init)
    parser_init.add_argument('-c', '--confdir', help='the config directory', type=str, required=confdir_required)
    parser_init.add_argument('-d', '--dir', help='the parent directory to put seafile-data', type=str)

    # start
    parser_start = subparsers.add_parser('start',
                                         help='Start ccnet and seafile daemon')
    parser_start.set_defaults(func=seaf_start_all)
    parser_start.add_argument('-c', '--confdir', help='the config directory', type=str, required=confdir_required)

    # stop
    parser_stop = subparsers.add_parser('stop',
                                         help='Stop ccnet and seafile daemon')
    parser_stop.set_defaults(func=seaf_stop)
    parser_stop.add_argument('-c', '--confdir', help='the config directory', type=str, required=confdir_required)

    # list
    parser_list = subparsers.add_parser('list', help='List local libraries')
    parser_list.set_defaults(func=seaf_list)
    parser_list.add_argument('-c', '--confdir', help='the config directory', type=str, required=confdir_required)

    # status
    parser_status = subparsers.add_parser('status', help='Show syncing status')
    parser_status.set_defaults(func=seaf_status)
    parser_status.add_argument('-c', '--confdir', help='the config directory', type=str, required=confdir_required)

    # download
    parser_download = subparsers.add_parser('download',
                                         help='Download a library from seafile server')
    parser_download.set_defaults(func=seaf_download)
    parser_download.add_argument('-c', '--confdir', help='the config directory', type=str, required=confdir_required)
    parser_download.add_argument('-l', '--library', help='library id', type=str)
    parser_download.add_argument('-s', '--server', help='URL for seafile server', type=str)
    parser_download.add_argument('-d', '--dir', help='the directory to put the library', type=str)
    parser_download.add_argument('-u', '--username', help='username', type=str)
    parser_download.add_argument('-p', '--password', help='password', type=str)


    # sync
    parser_sync = subparsers.add_parser('sync',
                                        help='Sync a library with an existing foler')
    parser_sync.set_defaults(func=seaf_sync)
    parser_sync.add_argument('-c', '--confdir', help='the config directory', type=str, required=confdir_required)
    parser_sync.add_argument('-l', '--library', help='library id', type=str)
    parser_sync.add_argument('-s', '--server', help='URL for seafile server', type=str)
    parser_sync.add_argument('-u', '--username', help='username', type=str)
    parser_sync.add_argument('-p', '--password', help='password', type=str)
    parser_sync.add_argument('-d', '--folder', help='the existing local folder', type=str)

    # desync
    parser_desync = subparsers.add_parser('desync',
                                          help='Desync a library with seafile server')
    parser_desync.set_defaults(func=seaf_desync)
    parser_desync.add_argument('-c', '--confdir', help='the config directory', type=str, required=confdir_required)
    parser_desync.add_argument('-d', '--folder', help='the local folder', type=str)

    # create
    parser_create = subparsers.add_parser('create',
                                          help='Create a library')
    parser_create.set_defaults(func=seaf_create)
    parser_create.add_argument('-n', '--name', help='library name', type=str)
    parser_create.add_argument('-t', '--desc', help='library description', type=str)
    parser_create.add_argument('-e', '--libpasswd', help='library password', type=str)
    parser_create.add_argument('-s', '--server', help='URL for seafile server', type=str)
    parser_create.add_argument('-u', '--username', help='username', type=str)
    parser_create.add_argument('-p', '--password', help='password', type=str)
    parser_create.add_argument('-c', '--confdir', help='the config directory', type=str, required=confdir_required)

    # config 
    parser_config = subparsers.add_parser('config',
                                          help='Configure seafile client')
    parser_config.set_defaults(func=seaf_config)
    parser_config.add_argument('-c', '--confdir', help='the config directory', type=str, required=confdir_required)
    parser_config.add_argument('-k', '--key', help='configuration key', type=str)
    parser_config.add_argument('-v', '--value', help='configuration value (if provided, key is set to this value)', type=str, required=False)

    if len(sys.argv) == 1:
        print parser.format_help()
        return

    args = parser.parse_args()
    args.func(args)


if __name__ == '__main__':
    main()
